<?php
/**
 * PHP Framework ICQ client node
 *
 * Implemented patterns: Intellect, Active Object, Message Queue
 *
 * The node interacts according to the following protocol:
 *
 * read {uin, password}
 * write {uin, password, to}
 * write {uin, password, to, reason}
 * write {uin, password, to, text}
 * write {uin, password, search, *}
 * write {uin, password, search, page=all, *}
 * write {uin, password, clear=messages}
 * write {uin, password, clear=searchResults}
 *
 * @author Andrew <andrew@vitche.com>
 * @author Frozen Rain <frozenrain@mail.ru>
 **/
class IcqNode extends Thread {

    /**
     * @var WebIcqPro A reference to the ICQ client class
     **/
    private $_client = null;

    /**
     * ICQ connection context
     * @var IcqContext
     **/
    private $_context = null;

    /**
     * Creates the IcqNode class
     * @param IcqContext $IcqContext
     **/
    public function IcqNode($IcqContext) {
        $this->_context = $IcqContext;
        // TODO: Make the client restoreable and backup-able
        $this->_client = new WebIcqPro();
    }

    /**
     * Builds the thread file name
     * @param Guid $id The thread identifier
     **/
    public function buildThreadFileName($id = null) {
        $uin = $this->_context->getUin();
        return "Framework.Intellect.IcqNode.$uin";
    }

    public function activate() {
        $this->start();
    }

    /**
     * Reads all messages from the queue
     *
     * Implements the following part of the protocol:
     * read {uin, password}
     *
     * @return Array
     **/
    public function read() {

        $file = fopen($this->buildThreadFileName(), "r");
        flock($file, LOCK_SH);

        $context = IcqContext::load($this->buildThreadFileName());
        if (null != $context) {
            $this->_context = $context;
        }

        $messages = $this->_context->getMessages();
        $searchResults = $this->_context->getSearchResults();

        $results = array_merge($messages, $searchResults);

        flock($file, LOCK_UN);
        fclose($file);

        return JSONSerializer::toString($results);
    }

    /**
     *
     * Writes node data
     *
     * Implements the following part of the protocol:
     * write {uin, password, to}
     * write {uin, password, to, reason}
     * write {uin, password, to, text}
     * write {uin, password, search, *}
     * write {uin, password, search, page=all, *}
     * write {uin, password, clear=messages}
     * write {uin, password, clear=searchResults}
     *
     * @param Array $data
     * @return Mixed
     **/
    public function write($data) {

        if (isset($data['to']) && isset($data['text'])) {
            // Implements the following part of the protocol
            // write {uin, password, to, text}
            $this->onTextMessage($data['to'], null, $data['text'], null);
            return JSONSerializer::toString(true);
        } else if (isset($data['to'])) {
            // Implements the following part of the protocol
            // write {uin, password, to}
            // write {uin, password, to, reason}
            $reason = null;
            if (isset($data['reason'])) {
                $reason = $data['reason'];
            }

            $result = $this->onAdd($data['to'], $reason);
            return JSONSerializer::toString($result);
        } else if (isset($data["search"])) {
            // Implements the following part of the protocol
            // write {uin, password, search, *}
            $arguments = $data;
            unset($arguments['uin']);
            unset($arguments['password']);
            unset($arguments['search']);
            $result = $this->onSearch($arguments);
            return JSONSerializer::toString($result);
        } else if (isset($data['clear'])) {
            // Implements the following part of the protocol
            // write {uin, password, clear=messages}
            // write {uin, password, clear=searchResults}
            $arguments = $data;
            unset($arguments['uin']);
            unset($arguments['password']);
            $result = $this->onClear($arguments);
            return JSONSerializer::toString($result);
        }

        return JSONSerializer::toString(false);
    }

    public function process() {

        // Connect if not connected yet
        if (!$this->_client->isConnected()) {
            if (!$this->_client->connect($this->_context->getUin(), $this->_context->getPassword())) {
                sleep(30);
                return;
            } else {
                $this->_client->setEncoding("UNICODE");
            }
        }

        if ($this->_client->isConnected()) {

            // Process messages coming from ICQ connection
            $message = $this->_client->readMessage();
            if ($message) {

                // Received a message from ICQ connection
                if ('message' == $message['type']) {

                    $this->onTextMessage(
                        null,
                        $message['from'],
                        $message['message'],
                        $message['encoding']['numset']);
                }

                // Dump message to debug console
                echo "<pre>";
                print_r($message);
                echo "</pre>";
                echo "<hr />";
                flush();
            }

            // Process messages coming from the message queue
            $this->onMessageQueue();
        }
    }

    /**
     * Tries to add the current context to the contact list of the given user
     * @param String $to A UIN of the user to whom the context is being added
     * @param String $reason A reason why the context is being added to the contact list of the given user
     * @return Boolean
     **/
    private function onAdd($to, $reason) {

        $threadFileName = $this->buildThreadFileName();
        if (file_exists($threadFileName)) {

            $file = fopen($threadFileName, "a");
            flock($file, LOCK_EX);

            $context = IcqContext::load($threadFileName);
            if (null != $context) {
                $this->_context = $context;
            }

            $IcqAuthorizationRequest = new IcqAuthorizationRequest();
            $IcqAuthorizationRequest->to = $to;
            $IcqAuthorizationRequest->reason = $reason;
            $context->addAuthorizationRequest($IcqAuthorizationRequest);

            IcqContext::save($threadFileName, $this->_context);
            flock($file, LOCK_UN);
            fclose($file);
        }

        return true;
    }

    /**
     * Called when a text message is received from ICQ or sent with REST
     * @param String $to A UIN to which the message is sent
     * @param String $from A UIN from which the message is sent
     * @param String $text Message text
     * @param String $encoding Message encoding
     **/
    private function onTextMessage($to, $from, $text, $encoding) {

        // Convert any incoming text to UTF-8
        if ("UNICODE" == $encoding && null == $to) {

            // Received a message
            $text = mb_convert_encoding($text, "UTF-8", "UTF-16");
        }

        // TODO: Check for file existance
        $file = fopen($this->buildThreadFileName(), "a");
        flock($file, LOCK_EX);

        $context = IcqContext::load($this->buildThreadFileName());
        if (null != $context) {
            $this->_context = $context;
        }

        $IcqMessage = new IcqMessage();
        if (null == $to) {
            // Received a message
            $IcqMessage->to = $this->_context->getUin();
        } else {
            // Sent a message
            $IcqMessage->outgoing = true;
            $IcqMessage->to = $to;
        }

        if (null == $from) {
            $IcqMessage->from = $this->_context->getUin();
        } else {
            $IcqMessage->from = $from;
        }

        $IcqMessage->text = $text;
        $this->_context->addMessage($IcqMessage);

        IcqContext::save($this->buildThreadFileName(), $this->_context);

        flock($file, LOCK_UN);
        fclose($file);
    }

    /**
     * Clears the given part of the node's state
     * @param Array $data
     **/
    private function onClear($data) {

        $modified = false;
        $threadFileName = $this->buildThreadFileName();
        if (file_exists($threadFileName)) {

            $file = fopen($threadFileName, "a");
            flock($file, LOCK_EX);

            $context = IcqContext::load($threadFileName);
            if (null != $context) {
                $this->_context = $context;
            }

            if ("messages" == $data["clear"]) {
                $this->_context->clearMessages();
                $modified = true;
            } else if ("searchResults" == $data["clear"]) {
                $this->_context->clearSearchResults();
                $modified = true;
            }

            if ($modified) {
                IcqContext::save($threadFileName, $this->_context);
            }

            flock($file, LOCK_UN);
            fclose($file);
        }

        return true;
    }

    /**
     * Called to process outgoing messages
     **/
    private function onMessageQueue() {

        $threadFileName = $this->buildThreadFileName();
        if (file_exists($threadFileName)) {

            $file = fopen($threadFileName, "a");
            flock($file, LOCK_EX);

            $context = IcqContext::load($threadFileName);
            if (null != $context) {
                $this->_context = $context;
            }

            // Locals
            $i = 0;
            $modified = false;

            // List all messages in the context of the current thread
            $messages = $this->_context->getMessages();
            while($i < count($messages)) {

                $message = $messages[$i];
                if ($message->outgoing) {

                    // Send message
                    $text = mb_convert_encoding($message->text, "UTF-16", "UTF-8");
                    $this->_client->sendMessage($message->to, $text);

                    // Delete message from queue
                    $this->_context->deleteMessage($i);

                    // Reset iterator
                    $modified = true;
                    $i = 0;
                    $messages = $this->_context->getMessages();
                }
            }

            // List all authorization requests in the context of the current thread
            $i = 0;
            $authorizationRequests = $this->_context->getAuthorizationRequests();
            while ($i < count($authorizationRequests)) {

                $authorizationRequest = $authorizationRequests[$i];
                $to = $authorizationRequest->to;
                $fromEncoding = mb_detect_encoding($authorizationRequest->reason);
                $reason = mb_convert_encoding($authorizationRequest->reason, "UTF-16", $fromEncoding);

                // Process authorization request
                $this->_client->addContact("General", array('uin' => $to, 'name' => $to));
                $this->_client->getAuthorization($to, $reason);

                // Delete authorization request from queue
                $this->_context->deleteAuthorizationRequest($i);

                // Reset iterator
                $modified = true;
                $i = 0;
                $authorizationRequests = $this->_context->getAuthorizationRequests();
            }

            // List all search requests in the context of the current thread
            $i = 0;
            $searchRequests = $this->_context->getSearchRequests();
            while ($i < count($searchRequests)) {

                // Extract the given search request
                $searchRequest = $searchRequests[$i];

                // Process search request
                $IcqSearchHTTPWebServiceReference = new IcqSearchHTTPWebServiceReference();
                $request = $searchRequest->toArray();
                $results = array();
                if (isset($request['page']) && 'all' == $request['page']) {
                    $results = $IcqSearchHTTPWebServiceReference->recurrentSearch($request);
                } else {
                    $results = $IcqSearchHTTPWebServiceReference->search($request);
                }

                // Keep search results
                $this->_context->addSearchResultRange($results);

                // Delete search request from queue
                $this->_context->deleteSearchRequest($i);

                // Reset iterator
                $modified = true;
                $i = 0;
                $searchRequests = $this->_context->getSearchRequests();
            }

            if ($modified) {
                IcqContext::save($threadFileName, $this->_context);
            }

            flock($file, LOCK_UN);
            fclose($file);
        }
    }

    /**
     * Called on a search request
     * @param Array $data
     * @return Boolean
     **/
    private function onSearch($data) {

        $threadFileName = $this->buildThreadFileName();
        if (file_exists($threadFileName)) {

            $file = fopen($threadFileName, "a");
            flock($file, LOCK_EX);

            $context = IcqContext::load($this->buildThreadFileName());
            if (null != $context) {
                $this->_context = $context;
            }

            // Build search request
            $searchRequest = new IcqSearchRequest($data);
            $this->_context->addSearchRequest($searchRequest);

            IcqContext::save($threadFileName, $this->_context);
            flock($file, LOCK_UN);
            fclose($file);
        }

        return true;
    }
}
?>